﻿using System;
using System.Collections.Generic;
using System.IO;
using SSG.Tools;

namespace SSG.Model
{
    /// <summary>
    /// Route
    /// </summary>
    public class Route : BaseEntity
    {
        private readonly List<Fix> _fixes = new List<Fix>();
        private readonly List<Leg> _legs = new List<Leg>();

        /// <summary>
        /// Fixes
        /// </summary>
        public List<Fix> Fixes { get { return this._fixes; } }
        /// <summary>
        /// Fixes with altitude
        /// </summary>
        public List<Fix> FixesWithAltitude { get { return this.Fixes.FixesWithAltitude(); } }
        /// <summary>
        /// Any fixes with altitude
        /// </summary>
        public bool HasFixesWithAltitude { get { return this.Fixes.HasFixesWithAltitude(); } }
        /// <summary>
        /// Altitude set if no altitude is available
        /// </summary>
        public Altitude DefaultAltitude { get; set; }
        /// <summary>
        /// Legs
        /// </summary>
        public List<Leg> Legs { get { return this._legs; } }
        /// <summary>
        /// Maximum altitude
        /// </summary>
        public Altitude MaxAltitude { get { return this.Fixes.MaxAltitude(); } }
        /// <summary>
        /// Minimum altitude
        /// </summary>
        public Altitude MinAltitude { get { return this.Fixes.MinAltitude(); } }
        /// <summary>
        /// Runway altitude if applicable
        /// </summary>
        public Altitude RunwayAltitude { get { return this.Fixes.FindRunwayAltitude(); } }
        /// <summary>
        /// Estimate some maximum for a climb calculation
        /// </summary>
        public Altitude EstimatedMaxClimb
        {
            get
            {
                // Max climb value
                double maxClimb = 10000.0;
                Altitude runwayAltitude = this.RunwayAltitude;
                runwayAltitude = runwayAltitude ?? Altitude.Altitude0;
                Altitude maxAltitude = this.Fixes.MaxAltitudeRestriction();
                if (runwayAltitude != null) maxClimb += (Math.Round(runwayAltitude.ValueFt/1000)*1000);
                if (maxAltitude == null || maxClimb > maxAltitude.ValueFt) return new Altitude(maxClimb, Altitude.AltitudeMode.Msl, Altitude.AltitudeModifier.Exact, true);
                return maxAltitude;
            }
        }

        /// <summary>
        /// Corresponding type as <see cref="Sid"/>, <see cref="Star"/>, <see cref="Transition"/>, <see cref="Approach"/>
        /// </summary>
        public Type CorrespondingType { get; set; }
        /// <summary>
        /// Settings
        /// </summary>
        public GeneratorSettings Settings { get; set; }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="name"></param>
        /// <param name="canonicalName"></param>
        /// <param name="description"></param>
        public Route(string name, string canonicalName = null, string description = null) : base(name, canonicalName, description)
        {
            this.DefaultAltitude = null;
            this.Settings = new GeneratorSettings();
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="calculate"></param>
        /// <param name="name"></param>
        /// <param name="canonicalName"></param>
        /// <param name="description"></param>
        public Route(List<Fix> fixes, bool calculate, string name, string canonicalName = null, string description = null) : this(name, canonicalName, description)
        {
            this.AddFixes(fixes);
            if (calculate && !fixes.NullSafeIsEmpty()) {
                this.Calculate();
            }
        }

        /// <summary>
        /// Add a fixpoint
        /// </summary>
        /// <param name="fixpoint"></param>
        /// <param name="clone"></param>
        public void AddFix(Fix fixpoint, bool clone = true)
        {
            this.Fixes.AddFix(fixpoint, clone);
        }

        /// <summary>
        /// Add fixpoints
        /// </summary>
        /// <param name="fixpoints"></param>
        /// <param name="clone"></param>
        public void AddFixes(List<Fix> fixpoints, bool clone = true)
        {
            this.Fixes.AddFixes(fixpoints, clone);
        }

        /// <summary>
        /// Insert a fixpoint
        /// </summary>
        /// <param name="fixpoint"></param>
        /// <param name="clone"></param>
        public void InsertFix(Fix fixpoint, bool clone = true)
        {
            this.Fixes.InsertFix(fixpoint, clone);
        }

        /// <summary>
        /// Insert fixpoints
        /// </summary>
        /// <param name="fixpoints"></param>
        /// <param name="clone"></param>
        public void InsertFixes(List<Fix> fixpoints, bool clone = true)
        {
            this.Fixes.InsertFixes(fixpoints, clone);
        }

        /// <summary>
        /// Legs and altitudes
        /// </summary>
        public void Calculate()
        {
            this.CalculateAltitudes();
            this.CalculateLegs();
        }

        /// <summary>
        /// Calculate legs
        /// </summary>
        private void CalculateLegs()
        {
            try {
                List<Leg> legs = this.Fixes.CalculateLegs();
                this.Legs.Clear();
                this.Legs.AddRange(legs);
            }
            catch (Exception e) {
                string msg = this.GetClassname();
                msg += String.IsNullOrEmpty(this.CanonicalName) ? "" : " " + this.CanonicalName;
                throw new InvalidOperationException("Calculating legs failed: " + msg, e);
            }
        }

        /// <summary>
        /// Calculate altitudes
        /// </summary>
        private void CalculateAltitudes()
        {
            try {
                if (!this.HasFixesWithAltitude) {
                    if (this.DefaultAltitude != null) this.Fixes.SetAltitudeCalculated(this.DefaultAltitude);
                    return;
                }

                // backward propagation
                int firstFixWithAltitudeIndex = this.Fixes.FirstFixWithAltitudeIndex();
                if (firstFixWithAltitudeIndex > 0) {
                    Altitude backwardAlt = this.Fixes[firstFixWithAltitudeIndex].Altitude;
                    if (backwardAlt == null) throw new InvalidDataException("Invalid altitude, must not be null");
                    for (int f = 0; f < firstFixWithAltitudeIndex && f < this.Fixes.Count; f++) {
                        Altitude a = new Altitude(backwardAlt) {IsCalculated = true};
                        this.Fixes[f].AltitudeCalculated = a;
                    }
                }

                // forward calculation
                int lastFixWithAltitudeIndex = firstFixWithAltitudeIndex;
                int lastIndex = this.Fixes.Count - 1;
                for (int f = firstFixWithAltitudeIndex + 1; f <= lastIndex; f++) {
                    Fix currentFix = this.Fixes[f];
                    if (currentFix.Altitude != null) {
                        // fixpoint has already altitude
                        lastFixWithAltitudeIndex = f;
                        continue;
                    }

                    // no more altitudes?
                    Fix lastFixWithAltitude = this.Fixes[lastFixWithAltitudeIndex];
                    int nextAltitudeIndex = this.Fixes.FirstFixWithAltitudeIndex(f + 1);
                    if (nextAltitudeIndex < f) {
                        // no more altitudes found
                        // propagate last altitude
                        List<Fix> restFixes = this.Fixes.GetRange(f, this.Fixes.Count - f);
                        if (this.CorrespondingType == typeof (Sid)) {
                            restFixes.CalculateClimbOrDescent(lastFixWithAltitude, this.Settings.ClimbRateFtPerKm, this.EstimatedMaxClimb.ValueFt);
                        }
                        else {
                            restFixes.SetAltitudeCalculated(this.Fixes[lastFixWithAltitudeIndex].Altitude);
                        }
                        break;
                    }

                    // calculate altitudes
                    List<Fix> fixAltRange = this.Fixes.GetRange(lastFixWithAltitudeIndex, nextAltitudeIndex - lastFixWithAltitudeIndex + 1);
                    Fix nextFixWithAlt = this.Fixes[nextAltitudeIndex];
                    double deltaAltFt = lastFixWithAltitude.Altitude.ValueFt - nextFixWithAlt.Altitude.ValueFt;
                    double deltaDistance = fixAltRange.DistanceKm();
                    double rateFtPerKm = -(deltaAltFt/deltaDistance); // forward looking
                    for (int ff = lastFixWithAltitudeIndex + 1; ff < nextAltitudeIndex; ff++) {
                        Fix toBeCalculated = this.Fixes[ff];
                        double deltaToLastFix = lastFixWithAltitude.ShortestDistanceKm(toBeCalculated);
                        toBeCalculated.AltitudeCalculated = new Altitude(lastFixWithAltitude.Altitude.ValueFt + (deltaToLastFix*rateFtPerKm), Altitude.AltitudeMode.Msl, Altitude.AltitudeModifier.Exact, true);
                        lastFixWithAltitude = toBeCalculated;
                    }

                    // continue after calculation
                    lastFixWithAltitudeIndex = nextAltitudeIndex;
                    f = nextAltitudeIndex;
                }
            }
            catch (Exception e) {
                string msg = this.GetClassname();
                msg += String.IsNullOrEmpty(this.CanonicalName) ? "" : " " + this.CanonicalName;
                throw new InvalidOperationException("Calculating altitudes failed: " + msg, e);
            }
        }

        /// <summary>
        /// Meaningful representation
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return "Route, fixes: " + this.Fixes.NullSafeCount() + " , legs: " + this.Legs.NullSafeCount();
        }
    }
}